/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.nineoldandroids.view;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;

import android.view.View;
import android.view.animation.Interpolator;

import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ValueAnimator;
import com.nineoldandroids.view.animation.AnimatorProxy;

class ViewPropertyAnimatorPreHC extends ViewPropertyAnimator {
	/**
	 * Proxy animation class which will allow us access to post-Honeycomb
	 * properties that were not otherwise available.
	 */
	private final AnimatorProxy mProxy;

	/**
	 * A WeakReference holding the View whose properties are being animated by
	 * this class. This is set at construction time.
	 */
	private final WeakReference<View> mView;

	/**
	 * The duration of the underlying Animator object. By default, we don't set
	 * the duration on the Animator and just use its default duration. If the
	 * duration is ever set on this Animator, then we use the duration that it
	 * was set to.
	 */
	private long mDuration;

	/**
	 * A flag indicating whether the duration has been set on this object. If
	 * not, we don't set the duration on the underlying Animator, but instead
	 * just use its default duration.
	 */
	private boolean mDurationSet = false;

	/**
	 * The startDelay of the underlying Animator object. By default, we don't
	 * set the startDelay on the Animator and just use its default startDelay.
	 * If the startDelay is ever set on this Animator, then we use the
	 * startDelay that it was set to.
	 */
	private long mStartDelay = 0;

	/**
	 * A flag indicating whether the startDelay has been set on this object. If
	 * not, we don't set the startDelay on the underlying Animator, but instead
	 * just use its default startDelay.
	 */
	private boolean mStartDelaySet = false;

	/**
	 * The interpolator of the underlying Animator object. By default, we don't
	 * set the interpolator on the Animator and just use its default
	 * interpolator. If the interpolator is ever set on this Animator, then we
	 * use the interpolator that it was set to.
	 */
	private/* Time */Interpolator mInterpolator;

	/**
	 * A flag indicating whether the interpolator has been set on this object.
	 * If not, we don't set the interpolator on the underlying Animator, but
	 * instead just use its default interpolator.
	 */
	private boolean mInterpolatorSet = false;

	/**
	 * Listener for the lifecycle events of the underlying
	 */
	private Animator.AnimatorListener mListener = null;

	/**
	 * This listener is the mechanism by which the underlying Animator causes
	 * changes to the properties currently being animated, as well as the
	 * cleanup after an animation is complete.
	 */
	private AnimatorEventListener mAnimatorEventListener = new AnimatorEventListener();

	/**
	 * This list holds the properties that have been asked to animate. We allow
	 * the caller to request several animations prior to actually starting the
	 * underlying animator. This enables us to run one single animator to handle
	 * several properties in parallel. Each property is tossed onto the pending
	 * list until the animation actually starts (which is done by posting it
	 * onto mView), at which time the pending list is cleared and the properties
	 * on that list are added to the list of properties associated with that
	 * animator.
	 */
	ArrayList<NameValuesHolder> mPendingAnimations = new ArrayList<NameValuesHolder>();

	/**
	 * Constants used to associate a property being requested and the mechanism
	 * used to set the property (this class calls directly into View to set the
	 * properties in question).
	 */
	private static final int NONE = 0x0000;
	private static final int TRANSLATION_X = 0x0001;
	private static final int TRANSLATION_Y = 0x0002;
	private static final int SCALE_X = 0x0004;
	private static final int SCALE_Y = 0x0008;
	private static final int ROTATION = 0x0010;
	private static final int ROTATION_X = 0x0020;
	private static final int ROTATION_Y = 0x0040;
	private static final int X = 0x0080;
	private static final int Y = 0x0100;
	private static final int ALPHA = 0x0200;

	private static final int TRANSFORM_MASK = TRANSLATION_X | TRANSLATION_Y
			| SCALE_X | SCALE_Y | ROTATION | ROTATION_X | ROTATION_Y | X | Y;

	/**
	 * The mechanism by which the user can request several properties that are
	 * then animated together works by posting this Runnable to start the
	 * underlying Animator. Every time a property animation is requested, we
	 * cancel any previous postings of the Runnable and re-post it. This means
	 * that we will only ever run the Runnable (and thus start the underlying
	 * animator) after the caller is done setting the properties that should be
	 * animated together.
	 */
	private Runnable mAnimationStarter = new Runnable() {
		@Override
		public void run() {
			startAnimation();
		}
	};

	/**
	 * This class holds information about the overall animation being run on the
	 * set of properties. The mask describes which properties are being animated
	 * and the values holder is the list of all property/value objects.
	 */
	private static class PropertyBundle {
		int mPropertyMask;
		ArrayList<NameValuesHolder> mNameValuesHolder;

		PropertyBundle(int propertyMask,
				ArrayList<NameValuesHolder> nameValuesHolder) {
			mPropertyMask = propertyMask;
			mNameValuesHolder = nameValuesHolder;
		}

		/**
		 * Removes the given property from being animated as a part of this
		 * PropertyBundle. If the property was a part of this bundle, it returns
		 * true to indicate that it was, in fact, canceled. This is an
		 * indication to the caller that a cancellation actually occurred.
		 * 
		 * @param propertyConstant
		 *            The property whose cancellation is requested.
		 * @return true if the given property is a part of this bundle and if it
		 *         has therefore been canceled.
		 */
		boolean cancel(int propertyConstant) {
			if ((mPropertyMask & propertyConstant) != 0
					&& mNameValuesHolder != null) {
				int count = mNameValuesHolder.size();
				for (int i = 0; i < count; ++i) {
					NameValuesHolder nameValuesHolder = mNameValuesHolder
							.get(i);
					if (nameValuesHolder.mNameConstant == propertyConstant) {
						mNameValuesHolder.remove(i);
						mPropertyMask &= ~propertyConstant;
						return true;
					}
				}
			}
			return false;
		}
	}

	/**
	 * This list tracks the list of properties being animated by any particular
	 * animator. In most situations, there would only ever be one animator
	 * running at a time. But it is possible to request some properties to
	 * animate together, then while those properties are animating, to request
	 * some other properties to animate together. The way that works is by
	 * having this map associate the group of properties being animated with the
	 * animator handling the animation. On every update event for an Animator,
	 * we ask the map for the associated properties and set them accordingly.
	 */
	private HashMap<Animator, PropertyBundle> mAnimatorMap = new HashMap<Animator, PropertyBundle>();

	/**
	 * This is the information we need to set each property during the
	 * animation. mNameConstant is used to set the appropriate field in View,
	 * and the from/delta values are used to calculate the animated value for a
	 * given animation fraction during the animation.
	 */
	private static class NameValuesHolder {
		int mNameConstant;
		float mFromValue;
		float mDeltaValue;

		NameValuesHolder(int nameConstant, float fromValue, float deltaValue) {
			mNameConstant = nameConstant;
			mFromValue = fromValue;
			mDeltaValue = deltaValue;
		}
	}

	/**
	 * Constructor, called by View. This is private by design, as the user
	 * should only get a ViewPropertyAnimator by calling View.animate().
	 * 
	 * @param view
	 *            The View associated with this ViewPropertyAnimator
	 */
	ViewPropertyAnimatorPreHC(View view) {
		mView = new WeakReference<View>(view);
		mProxy = AnimatorProxy.wrap(view);
	}

	/**
	 * Sets the duration for the underlying animator that animates the requested
	 * properties. By default, the animator uses the default value for
	 * ValueAnimator. Calling this method will cause the declared value to be
	 * used instead.
	 * 
	 * @param duration
	 *            The length of ensuing property animations, in milliseconds.
	 *            The value cannot be negative.
	 * @return This object, allowing calls to methods in this class to be
	 *         chained.
	 */
	public ViewPropertyAnimator setDuration(long duration) {
		if (duration < 0) {
			throw new IllegalArgumentException(
					"Animators cannot have negative duration: " + duration);
		}
		mDurationSet = true;
		mDuration = duration;
		return this;
	}

	/**
	 * Returns the current duration of property animations. If the duration was
	 * set on this object, that value is returned. Otherwise, the default value
	 * of the underlying Animator is returned.
	 * 
	 * @see #setDuration(long)
	 * @return The duration of animations, in milliseconds.
	 */
	public long getDuration() {
		if (mDurationSet) {
			return mDuration;
		} else {
			// Just return the default from ValueAnimator, since that's what
			// we'd get if
			// the value has not been set otherwise
			return new ValueAnimator().getDuration();
		}
	}

	@Override
	public long getStartDelay() {
		if (mStartDelaySet) {
			return mStartDelay;
		} else {
			// Just return the default from ValueAnimator (0), since that's what
			// we'd get if
			// the value has not been set otherwise
			return 0;
		}
	}

	@Override
	public ViewPropertyAnimator setStartDelay(long startDelay) {
		if (startDelay < 0) {
			throw new IllegalArgumentException(
					"Animators cannot have negative duration: " + startDelay);
		}
		mStartDelaySet = true;
		mStartDelay = startDelay;
		return this;
	}

	@Override
	public ViewPropertyAnimator setInterpolator(
	/* Time */Interpolator interpolator) {
		mInterpolatorSet = true;
		mInterpolator = interpolator;
		return this;
	}

	@Override
	public ViewPropertyAnimator setListener(Animator.AnimatorListener listener) {
		mListener = listener;
		return this;
	}

	@Override
	public void start() {
		startAnimation();
	}

	@SuppressWarnings("unchecked")
	@Override
	public void cancel() {
		if (mAnimatorMap.size() > 0) {
			HashMap<Animator, PropertyBundle> mAnimatorMapCopy = (HashMap<Animator, PropertyBundle>) mAnimatorMap
					.clone();
			Set<Animator> animatorSet = mAnimatorMapCopy.keySet();
			for (Animator runningAnim : animatorSet) {
				runningAnim.cancel();
			}
		}
		mPendingAnimations.clear();
		View v = mView.get();
		if (v != null) {
			v.removeCallbacks(mAnimationStarter);
		}
	}

	@Override
	public ViewPropertyAnimator x(float value) {
		animateProperty(X, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator xBy(float value) {
		animatePropertyBy(X, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator y(float value) {
		animateProperty(Y, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator yBy(float value) {
		animatePropertyBy(Y, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator rotation(float value) {
		animateProperty(ROTATION, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator rotationBy(float value) {
		animatePropertyBy(ROTATION, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator rotationX(float value) {
		animateProperty(ROTATION_X, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator rotationXBy(float value) {
		animatePropertyBy(ROTATION_X, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator rotationY(float value) {
		animateProperty(ROTATION_Y, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator rotationYBy(float value) {
		animatePropertyBy(ROTATION_Y, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator translationX(float value) {
		animateProperty(TRANSLATION_X, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator translationXBy(float value) {
		animatePropertyBy(TRANSLATION_X, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator translationY(float value) {
		animateProperty(TRANSLATION_Y, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator translationYBy(float value) {
		animatePropertyBy(TRANSLATION_Y, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator scaleX(float value) {
		animateProperty(SCALE_X, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator scaleXBy(float value) {
		animatePropertyBy(SCALE_X, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator scaleY(float value) {
		animateProperty(SCALE_Y, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator scaleYBy(float value) {
		animatePropertyBy(SCALE_Y, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator alpha(float value) {
		animateProperty(ALPHA, value);
		return this;
	}

	@Override
	public ViewPropertyAnimator alphaBy(float value) {
		animatePropertyBy(ALPHA, value);
		return this;
	}

	/**
	 * Starts the underlying Animator for a set of properties. We use a single
	 * animator that simply runs from 0 to 1, and then use that fractional value
	 * to set each property value accordingly.
	 */
	@SuppressWarnings("unchecked")
	private void startAnimation() {
		ValueAnimator animator = ValueAnimator.ofFloat(1.0f);
		ArrayList<NameValuesHolder> nameValueList = (ArrayList<NameValuesHolder>) mPendingAnimations
				.clone();
		mPendingAnimations.clear();
		int propertyMask = 0;
		int propertyCount = nameValueList.size();
		for (int i = 0; i < propertyCount; ++i) {
			NameValuesHolder nameValuesHolder = nameValueList.get(i);
			propertyMask |= nameValuesHolder.mNameConstant;
		}
		mAnimatorMap.put(animator, new PropertyBundle(propertyMask,
				nameValueList));
		animator.addUpdateListener(mAnimatorEventListener);
		animator.addListener(mAnimatorEventListener);
		if (mStartDelaySet) {
			animator.setStartDelay(mStartDelay);
		}
		if (mDurationSet) {
			animator.setDuration(mDuration);
		}
		if (mInterpolatorSet) {
			animator.setInterpolator(mInterpolator);
		}
		animator.start();
	}

	/**
	 * Utility function, called by the various x(), y(), etc. methods. This
	 * stores the constant name for the property along with the from/delta
	 * values that will be used to calculate and set the property during the
	 * animation. This structure is added to the pending animations, awaiting
	 * the eventual start() of the underlying animator. A Runnable is posted to
	 * start the animation, and any pending such Runnable is canceled (which
	 * enables us to end up starting just one animator for all of the properties
	 * specified at one time).
	 * 
	 * @param constantName
	 *            The specifier for the property being animated
	 * @param toValue
	 *            The value to which the property will animate
	 */
	private void animateProperty(int constantName, float toValue) {
		float fromValue = getValue(constantName);
		float deltaValue = toValue - fromValue;
		animatePropertyBy(constantName, fromValue, deltaValue);
	}

	/**
	 * Utility function, called by the various xBy(), yBy(), etc. methods. This
	 * method is just like animateProperty(), except the value is an offset from
	 * the property's current value, instead of an absolute "to" value.
	 * 
	 * @param constantName
	 *            The specifier for the property being animated
	 * @param byValue
	 *            The amount by which the property will change
	 */
	private void animatePropertyBy(int constantName, float byValue) {
		float fromValue = getValue(constantName);
		animatePropertyBy(constantName, fromValue, byValue);
	}

	/**
	 * Utility function, called by animateProperty() and animatePropertyBy(),
	 * which handles the details of adding a pending animation and posting the
	 * request to start the animation.
	 * 
	 * @param constantName
	 *            The specifier for the property being animated
	 * @param startValue
	 *            The starting value of the property
	 * @param byValue
	 *            The amount by which the property will change
	 */
	private void animatePropertyBy(int constantName, float startValue,
			float byValue) {
		// First, cancel any existing animations on this property
		if (mAnimatorMap.size() > 0) {
			Animator animatorToCancel = null;
			Set<Animator> animatorSet = mAnimatorMap.keySet();
			for (Animator runningAnim : animatorSet) {
				PropertyBundle bundle = mAnimatorMap.get(runningAnim);
				if (bundle.cancel(constantName)) {
					// property was canceled - cancel the animation if it's now
					// empty
					// Note that it's safe to break out here because every new
					// animation
					// on a property will cancel a previous animation on that
					// property, so
					// there can only ever be one such animation running.
					if (bundle.mPropertyMask == NONE) {
						// the animation is no longer changing anything - cancel
						// it
						animatorToCancel = runningAnim;
						break;
					}
				}
			}
			if (animatorToCancel != null) {
				animatorToCancel.cancel();
			}
		}

		NameValuesHolder nameValuePair = new NameValuesHolder(constantName,
				startValue, byValue);
		mPendingAnimations.add(nameValuePair);
		View v = mView.get();
		if (v != null) {
			v.removeCallbacks(mAnimationStarter);
			v.post(mAnimationStarter);
		}
	}

	/**
	 * This method handles setting the property values directly in the View
	 * object's fields. propertyConstant tells it which property should be set,
	 * value is the value to set the property to.
	 * 
	 * @param propertyConstant
	 *            The property to be set
	 * @param value
	 *            The value to set the property to
	 */
	private void setValue(int propertyConstant, float value) {
		// final View.TransformationInfo info = mView.mTransformationInfo;
		switch (propertyConstant) {
		case TRANSLATION_X:
			// info.mTranslationX = value;
			mProxy.setTranslationX(value);
			break;
		case TRANSLATION_Y:
			// info.mTranslationY = value;
			mProxy.setTranslationY(value);
			break;
		case ROTATION:
			// info.mRotation = value;
			mProxy.setRotation(value);
			break;
		case ROTATION_X:
			// info.mRotationX = value;
			mProxy.setRotationX(value);
			break;
		case ROTATION_Y:
			// info.mRotationY = value;
			mProxy.setRotationY(value);
			break;
		case SCALE_X:
			// info.mScaleX = value;
			mProxy.setScaleX(value);
			break;
		case SCALE_Y:
			// info.mScaleY = value;
			mProxy.setScaleY(value);
			break;
		case X:
			// info.mTranslationX = value - mView.mLeft;
			mProxy.setX(value);
			break;
		case Y:
			// info.mTranslationY = value - mView.mTop;
			mProxy.setY(value);
			break;
		case ALPHA:
			// info.mAlpha = value;
			mProxy.setAlpha(value);
			break;
		}
	}

	/**
	 * This method gets the value of the named property from the View object.
	 * 
	 * @param propertyConstant
	 *            The property whose value should be returned
	 * @return float The value of the named property
	 */
	private float getValue(int propertyConstant) {
		// final View.TransformationInfo info = mView.mTransformationInfo;
		switch (propertyConstant) {
		case TRANSLATION_X:
			// return info.mTranslationX;
			return mProxy.getTranslationX();
		case TRANSLATION_Y:
			// return info.mTranslationY;
			return mProxy.getTranslationY();
		case ROTATION:
			// return info.mRotation;
			return mProxy.getRotation();
		case ROTATION_X:
			// return info.mRotationX;
			return mProxy.getRotationX();
		case ROTATION_Y:
			// return info.mRotationY;
			return mProxy.getRotationY();
		case SCALE_X:
			// return info.mScaleX;
			return mProxy.getScaleX();
		case SCALE_Y:
			// return info.mScaleY;
			return mProxy.getScaleY();
		case X:
			// return mView.mLeft + info.mTranslationX;
			return mProxy.getX();
		case Y:
			// return mView.mTop + info.mTranslationY;
			return mProxy.getY();
		case ALPHA:
			// return info.mAlpha;
			return mProxy.getAlpha();
		}
		return 0;
	}

	/**
	 * Utility class that handles the various Animator events. The only ones we
	 * care about are the end event (which we use to clean up the animator map
	 * when an animator finishes) and the update event (which we use to
	 * calculate the current value of each property and then set it on the view
	 * object).
	 */
	private class AnimatorEventListener implements Animator.AnimatorListener,
			ValueAnimator.AnimatorUpdateListener {
		@Override
		public void onAnimationStart(Animator animation) {
			if (mListener != null) {
				mListener.onAnimationStart(animation);
			}
		}

		@Override
		public void onAnimationCancel(Animator animation) {
			if (mListener != null) {
				mListener.onAnimationCancel(animation);
			}
		}

		@Override
		public void onAnimationRepeat(Animator animation) {
			if (mListener != null) {
				mListener.onAnimationRepeat(animation);
			}
		}

		@Override
		public void onAnimationEnd(Animator animation) {
			if (mListener != null) {
				mListener.onAnimationEnd(animation);
			}
			mAnimatorMap.remove(animation);
			// If the map is empty, it means all animation are done or canceled,
			// so the listener
			// isn't needed anymore. Not nulling it would cause it to leak any
			// objects used in
			// its implementation
			if (mAnimatorMap.isEmpty()) {
				mListener = null;
			}
		}

		/**
		 * Calculate the current value for each property and set it on the view.
		 * Invalidate the view object appropriately, depending on which
		 * properties are being animated.
		 * 
		 * @param animation
		 *            The animator associated with the properties that need to
		 *            be set. This animator holds the animation fraction which
		 *            we will use to calculate the current value of each
		 *            property.
		 */
		@Override
		public void onAnimationUpdate(ValueAnimator animation) {
			// alpha requires slightly different treatment than the other
			// (transform) properties.
			// The logic in setAlpha() is not simply setting mAlpha, plus the
			// invalidation
			// logic is dependent on how the view handles an internal call to
			// onSetAlpha().
			// We track what kinds of properties are set, and how alpha is
			// handled when it is
			// set, and perform the invalidation steps appropriately.
			// boolean alphaHandled = false;
			// mView.invalidateParentCaches();
			float fraction = animation.getAnimatedFraction();
			PropertyBundle propertyBundle = mAnimatorMap.get(animation);
			int propertyMask = propertyBundle.mPropertyMask;
			if ((propertyMask & TRANSFORM_MASK) != 0) {
				View v = mView.get();
				if (v != null) {
					v.invalidate(/* false */);
				}
			}
			ArrayList<NameValuesHolder> valueList = propertyBundle.mNameValuesHolder;
			if (valueList != null) {
				int count = valueList.size();
				for (int i = 0; i < count; ++i) {
					NameValuesHolder values = valueList.get(i);
					float value = values.mFromValue + fraction
							* values.mDeltaValue;
					// if (values.mNameConstant == ALPHA) {
					// alphaHandled = mView.setAlphaNoInvalidation(value);
					// } else {
					setValue(values.mNameConstant, value);
					// }
				}
			}
			/*
			 * if ((propertyMask & TRANSFORM_MASK) != 0) {
			 * mView.mTransformationInfo.mMatrixDirty = true;
			 * mView.mPrivateFlags |= View.DRAWN; // force another invalidation
			 * }
			 */
			// invalidate(false) in all cases except if alphaHandled gets set to
			// true
			// via the call to setAlphaNoInvalidation(), above
			View v = mView.get();
			if (v != null) {
				v.invalidate(/* alphaHandled */);
			}
		}
	}
}
